/**
 * LoggerImpl 类核心功能总结
 * 日志文件格式
 *
 * 采用 [XChecksum] [Log1] [Log2] ... [LogN] [BadTail] 结构，XChecksum 是所有日志的校验和。
 * 每条日志 [Size] [Checksum] [Data]，分别代表数据长度、校验和、数据内容。
 * 日志初始化与校验
 *
 * 读取 XChecksum 并验证日志完整性。
 * 若 XChecksum 校验失败，说明日志文件损坏，需要截断无效部分。
 * 日志读取 (next())
 *
 * 依次读取 [Size]、[Checksum]、[Data]，校验数据完整性。
 * 发现损坏日志时终止读取，避免读取错误数据。
 * 日志写入 (log())
 *
 * 先计算 Checksum，再封装 [Size] [Checksum] [Data] 结构。
 * 写入日志后更新 XChecksum 以保证整体校验正确。
 * XChecksum 计算 (updateXChecksum())
 *
 * XChecksum 通过 SEED 进行哈希计算，保证日志一致性。
 * 写入 XChecksum 到文件头，并同步到磁盘。
 * 文件截断 (truncate())
 *
 * 若日志损坏，会截断 BadTail，确保日志文件的正确性。
 * 通过 FileChannel.truncate() 保留有效日志，去除错误部分。
 * 日志指针管理 (rewind())
 *
 * 读取日志时，通过 position 维护当前位置。
 * rewind() 将指针重置到 XChecksum 之后，准备读取。
 * 线程安全性
 *
 * 采用 ReentrantLock 确保多线程并发访问日志时的安全性。
 * 关键操作（写入、读取、截断）均加锁，避免数据竞争。
 */
package top.guoziyang.mydb.backend.dm.logger;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.google.common.primitives.Bytes;
import top.guoziyang.mydb.backend.utils.Panic;
import top.guoziyang.mydb.backend.utils.Parser;
import top.guoziyang.mydb.common.Error;

/**
 * 日志文件读写
 * 
 * 日志文件标准格式为：
 * [XChecksum] [Log1] [Log2] ... [LogN] [BadTail]
 * XChecksum 为后续所有日志计算的Checksum，int类型
 * 
 * 每条正确日志的格式为：
 * [Size] [Checksum] [Data]
 * Size 4字节int 标识Data长度
 * Checksum 4字节int
 */
public class LoggerImpl implements Logger {

    private static final int SEED = 13331;

    private static final int OF_SIZE = 0; // size的偏移量
    private static final int OF_CHECKSUM = OF_SIZE + 4; // XChecksum的偏移量
    private static final int OF_DATA = OF_CHECKSUM + 4; // data的偏移量
    
    public static final String LOG_SUFFIX = ".log";

    private RandomAccessFile file;//日志文件对象，用于文件读写
    private FileChannel fc; //文件通道 FileChannel，支持高效的 NIO 读写。
    private Lock lock;//ReentrantLock，用于保证线程安全。

    private long position;  // 当前日志指针的位置
    private long fileSize;  // 日志文件总大小，仅在 init() 读取时确定。
    private int xChecksum;//整个日志文件的校验和。

    /**
     * 该构造函数初始化 LoggerImpl，但 xChecksum 默认为 0
     */
    LoggerImpl(RandomAccessFile raf, FileChannel fc) {
        this.file = raf;
        this.fc = fc;
        lock = new ReentrantLock();
    }

    /***
     * 创建新的LoggerImpl时，xChecksum=0
     * 这个构造函数允许传入 xChecksum，用于恢复已有的日志文件。
     */
    LoggerImpl(RandomAccessFile raf, FileChannel fc, int xChecksum) {
        this.file = raf;
        this.fc = fc;
        this.xChecksum = xChecksum;
        lock = new ReentrantLock();
    }


    /***
     * 从现有Logger文件中读取数据创建LoggerImpl时，读取文件的xChecksum
     */
    void init() {
        long size = 0;
        try {
            size = file.length();
        } catch (IOException e) {
            Panic.panic(e);
        }
        //读取 xChecksum（文件头 4 字节）
        if(size < 4) {
            Panic.panic(Error.BadLogFileException);
        }

        //创建了一个容量为4字节的ByteBuffer对象raw，它用于存储从文件通道中读取的数据。
        ByteBuffer raw = ByteBuffer.allocate(4);
        try {
            fc.position(0);
            fc.read(raw);
        } catch (IOException e) {
            Panic.panic(e);
        }
        int xChecksum = Parser.parseInt(raw.array());
        this.fileSize = size;
        this.xChecksum = xChecksum;

        checkAndRemoveTail();
    }

    /***
     * 思路
     * 重置读取指针到日志开始位置。
     * 逐条读取日志条目，并累加校验和。
     * 如果累加后的校验和与存储的校验和一致，说明整个日志有效，无需处理。
     * 如果不一致，说明尾部有损坏，此时需要将文件截断到最后一个正确的位置（可能在读取过程中记录的position），然后更新文件指针到该位置，确保后续写入从正确的位置开始。
     */
    private void checkAndRemoveTail() {
        // 将文件位置指针重置到XChecksum之后，以便从头开始读取log数据
        rewind();
        // xCheck用于累加每个日志的校验和
        int xCheck = 0;
        // 逐条读取日志记录，直到文件读取完毕
        while(true) {
            byte[] log = internNext();
            if(log == null) break;
            // 将 log 的校验和与累加的 xCheck 值组合计算
            xCheck = calChecksum(xCheck, log);
        }
        // xCheck 计算出的校验和值应与文件初始的 xChecksum 值一致。若两者不等，说明日志文件已损坏。
        if(xCheck != xChecksum) {
            Panic.panic(Error.BadLogFileException);
        }
        // 调用 truncate(position) 截断文件，将文件大小限制到当前指针位置 position，去除尾部无效的数据。
        try {
            truncate(position);
        } catch (Exception e) {
            Panic.panic(e);
        }
        // 调用 file.seek(position) 将文件指针定位到 position 处（即文件末尾），为接下来的写入操作做好准备
        try {
            file.seek(position);
        } catch (IOException e) {
            Panic.panic(e);
        }
        rewind();
    }

    /***
     *  从日志文件中读取下一条日志记录
     */
    private byte[] internNext() {
        // 当前读取的位置+数据偏移量 > 文件末尾,表示没有更多日志记录可读
        //OF_DATA头部信息
        if(position + OF_DATA >= fileSize) {
            return null;
        }
        // 分配一个 4 字节的 ByteBuffer 来读取OF_DATA头部信息。
        ByteBuffer tmp = ByteBuffer.allocate(4);
        try {
            fc.position(position);
            fc.read(tmp);
        } catch(IOException e) {
            Panic.panic(e);
        }
        //解析出日志的大小 size
        int size = Parser.parseInt(tmp.array());
        // position + size + OF_DATA = 日志文件的末尾
        // 超出文件长度，说明当前日志文件不完整或损坏
        if(position + size + OF_DATA > fileSize) {
            return null;
        }

        // 分配 日志文件总容量大小 的缓存来读取整个日志文件
        ByteBuffer buf = ByteBuffer.allocate(OF_DATA + size);
        try {
            fc.position(position);
            fc.read(buf);
        } catch(IOException e) {
            Panic.panic(e);
        }
        byte[] log = buf.array();

        // 通过日志数据部分（从 OF_DATA 偏移量开始）计算得到的校验和
        int checkSum1 = calChecksum(0, Arrays.copyOfRange(log, OF_DATA, log.length));
        // 从日志记录的头部提取的预期校验和
        int checkSum2 = Parser.parseInt(Arrays.copyOfRange(log, OF_CHECKSUM, OF_DATA));
        // 若 checkSum1 与 checkSum2 不相等，表明数据不完整或已损坏，返回 null
        if(checkSum1 != checkSum2) {
            return null;
        }
        // 更新 position，以指向下一个日志记录的开始位置
        position += log.length;
        return log;
    }

    /***
     *  计算日志文件中data的校验和
     */
    private int calChecksum(int xCheck, byte[] log) {
        for (byte b : log) {
            xCheck = xCheck * SEED + b;
        }
        return xCheck;
    }

    /***
     * 日志写入,负责将数据封装成日志格式，然后写入文件
     */
    @Override
    public void log(byte[] data) {
        // 将数据封装成日志格式
        byte[] log = wrapLog(data);
        ByteBuffer buf = ByteBuffer.wrap(log);
        lock.lock();
        try {
            // 写入日志
            fc.position(fc.size());
            fc.write(buf);
        } catch(IOException e) {
            Panic.panic(e);
        } finally {
            lock.unlock();
        }
        // 更新整个日志的XChecksum
        updateXChecksum(log);
    }

    /***
     * 更新整个日志的XChecksum
     */
    private void updateXChecksum(byte[] log) {
        this.xChecksum = calChecksum(this.xChecksum, log);
        try {
            fc.position(0);
            fc.write(ByteBuffer.wrap(Parser.int2Byte(xChecksum)));
            fc.force(false); //强制将数据同步到磁盘，以确保数据的持久化
        } catch(IOException e) {
            Panic.panic(e);
        }
    }

    /***
     * 将数据封装成日志格式
     */
    private byte[] wrapLog(byte[] data) {
        // 计算数据的checksum
        byte[] checksum = Parser.int2Byte(calChecksum(0, data));
        byte[] size = Parser.int2Byte(data.length);
        return Bytes.concat(size, checksum, data);
    }


    /***
     * 截断文件，将文件大小限制到当前指针位置 position，去除尾部无效的数据
     */
    @Override
    public void truncate(long x) throws Exception {
        lock.lock();
        try {
            fc.truncate(x);
        } finally {
            lock.unlock();
        }
    }


    /***
     * 从日志文件中读取下一条日志的数据部分
     */
    @Override
    public byte[] next() {
        lock.lock();
        try {
            byte[] log = internNext();
            if(log == null) return null;
            return Arrays.copyOfRange(log, OF_DATA, log.length);
        } finally {
            lock.unlock();
        }
    }

    /***
     * 调整日志文件的指针到第一个日志条目
     */
    @Override
    public void rewind() {
        position = 4;
    }

    /***
     * 关闭文件流、通道流
     */
    @Override
    public void close() {
        try {
            fc.close();
            file.close();
        } catch(IOException e) {
            Panic.panic(e);
        }
    }
    
}
